<?php
m2import('phpinchains/Filter.php');
m2import('phpinchains/ActionForm.php');
m2import('phpinchains/ViewMapping.php');
m2import('phpinchains/WebAction.php');
m2import('phpinchains/SynchronizerToken.php');
m2import('phpinchains/util/DirTool.php');
m2import('phpinchains/ValidationErrors.php');

/**
 * Implements the action controller pattern and provides action mapping and view mapping capability.
 *
 * There are three modes of action dispatching:<br/>
 * <b>One class per mapping name:</b> Action class must extend WebAction and override execute method<br/>
 * <b>One class, many mappings:</b> Each mapping must be configured separately and has its own
 *      view mappings. For each mapping a method must be created with the same parameters as
 *      WebAction#execute<br/>
 * <b>One class, with method mapping:</b> With method mapping you can configure one class. Than it is
 *      possible to access each public method in this class<br/><br/>
 *
 * In case of an error 4 attributes are available in the errorview (responseScope):
 * <i>errorLocation:</i> Where did the error occur (which method of the ActionDispatcherFilter)<br/>
 * <i>errorCause:</i>    What caused the error.<br/>
 * <i>errorSubject:</i>  What was the subject of the error (e.g. what action method was tried to call)<br/>
 * <i>errorRequest:</i>  An ActionForm with all request parameters used for the dispatch.<br/><br/>
 *
 * <b>Configuration options:</b><br/>
 * <i>class-extension:</i>     File extension for class files<br/><br/>
 * <i>basedir:</i>             Base directory for module<br/><br/>
 * <i>view-by-methodname:</i>  Optional. If no view or view mapping is returned by action method, than
 *                             the method name is treated as viewmapping.<br/>
 *                             (values: true, false; default: false => is return value of action method
 *                             is an empty string, no view will be found)<br/><br/>
 * <i>bypass-mapping:</i>      Optional. Makes definition of actions and viewMappings in action-config.xml
 *                             optional. If bypass-mapping is set 'true', a not defined action or viewMapping
 *                             is handled, if it was defined:<br/>
 *                             mapping: classname without 'Action' suffix, first letter lowercase<br/>
 *                             class:   XxxAction, while xxx would be value of mapping<br/>
 *                             methodmapping: true<br/>
 *                             bypassViewMapping: true<br/>
 *                             If a differing definition is needed for a particular action, it
 *                             can be defined anyway.<br/><br/>
 * <i>no-view-error:</i>       Optional. Do not view error page if no view was specified.<br/>
 *                             (values: true, false; default: false => error is viewed)<br/><br/>
 * <i>use-compiledir:</i>      Optional. If not specified compiled file is stored in same directory
 *                             as corresponding source file<br/>
 * <i>compatibility-mode:</i>  Optional. If set true, PSPs for methodmapped actions are searched for in
 *                             a folder with the same name as the action class without modifications.<br/>
 * 							   In newer phpinchains versions they are searched for in folders named like
 * 							   the action class, but without the 'Action' at the end and with the first letter
 * 							   lowercase.<br/>
 * @package phpinchains
 */
class ActionDispatcherFilter extends phpinchains_Filter {

    var $basedirCache;
    var $actiondict;
    var $viewmapping;
    var $defaulterroraction;
    var $defaultaction;
    var $fileending;
    var $basedir;
    var $initbasedir;
    var $config;
    var $env;
    var $viewByMethod;
    var $noViewError;
    var $bypassMapping;
    var $compileDir = null;
    var $printOutput = true;
    var $ctx;

    /**
     * @param array $actions Assoz. array of action configurations
     */
    function addActions($actions) {
        foreach ($actions as $n => $v) {
            $this->registerAction($n, $v);
        }
    }

    /**
     * Register an action
     *
     * @param string $name Mapping name of the action
     * @param mixed $actionclass Array with class name and other parameters or string with class name (with path)
     */
    function registerAction($name, $actionclass) {
        $matches = array ();
        if (is_array($actionclass)) {
            $file = $actionclass['class'];
        }
        else {
            $file = $actionclass;
        }
        $this->actiondict[$name]['file'] = $actionclass;
        preg_match('/(\w+)$/', $file, $matches);
        $this->actiondict[$name]['class'] = $matches[1];
    }

    /**
     * Load the config file and configure the action controller environment
     *
     * @param array $config Array with configuration parameters, set in chain config
     */
    function init(& $config) {
        $ctx = & phpinchains_ApplicationContext::getInstance();
        $this->fileending    = $config['class-extension'];
        $this->initbasedir   = $this->basedir = $config['basedir'].'/';
        $ctx->setModuleBaseDir($this->initbasedir);
        $this->viewByMethod  = (isset($config['view-by-methodname']) && $config['view-by-methodname'] == 'true');
        $this->bypassMapping = (isset($config['bypass-mapping']) && $config['bypass-mapping'] == 'true');
        $this->noViewError   = (isset($config['no-view-error']) && $config['no-view-error'] == 'true');
        $this->compatibilityMode   = (isset($config['compatibility-mode']) && $config['compatibility-mode'] == 'true');
        if (!empty($config['use-compiledir']))
            $this->compileDir = $config['use-compiledir'].'/';
        $this->config       = & $config;
        $actionform         = new phpinchains_ActionForm();
        // caching of basedir for nested apps
        if (isset ($GLOBALS['MODEL2_BASEDIR']))
            $this->basedirCache = $GLOBALS['MODEL2_BASEDIR'];
        $GLOBALS['MODEL2_BASEDIR'] = $this->basedir;
        $actions = array();
        $viewmapping = array();
        $defaultaction = array();
        $defaulterroraction = array();
        PHPinChains :: compileConfigFile($config['basedir'].'/action-config.xml');
        require ($config['basedir'].'/action-config.xml.php');
        $this->addActions($actions);
        $this->viewmapping = new phpinchains_ViewMapping($viewmapping);
        $this->defaulterroraction = $defaulterroraction;
        $this->defaultaction = $defaultaction;
        $this->ctx = & phpinchains_ApplicationContext::getInstance();
        if (isset($config['additional-parameters'])) {
            $run = & phpinchains_RuntimeConfig::getInstance();
            $params = array();
            parse_str($config['additional-parameters'], $params);
            $run->set('additional_parameters', $params);
        }
    }

    /**
     * Compile dir set?
     *
     * @return boolean If compile dir was set
     */
    function hasCompileDir() {
        return ($this->compileDir!=null);
    }

    /**
     * Getter for compile dir
     *
     * @return string compile dir
     */
    function getCompileDir() {
        return $this->compileDir;
    }

    /**
     * Getter for initial basedir
     *
     * @return string initial basedir
     */
    function getInitialBaseDir() {
        return $this->initbasedir;
    }

    /**
     * Set content type to use in header
     *
     * @param string $type
     */
    function setContentType($type) {
        $cfgContentTypes['html']          = "text/html";
        $cfgContentTypes['svg']           = "image/svg+xml";
        $cfgContentTypes['xml']           = "text/xml";
        $cfgContentTypes['xhtml']         = "application/xhtml+xml";
        $cfgContentTypes['sgml']          = "application/sgml";
        $cfgContentTypes['txt']           = "text/plain";
        $cfgContentTypes['asc']           = "text/plain";
        $cfgContentTypes['tex']           = "application/x-tex";
        $cfgContentTypes['latex']         = "application/x-latex";
        $cfgContentTypes['jpeg']          = "image/jpeg";
        $cfgContentTypes['png']           = "image/png";
        $cfgContentTypes['sxw']           = "application/vnd.sun.xml.writer";
        $cfgContentTypes['sxc']           = "application/vnd.sun.xml.calc";
        $cfgContentTypes['sxi']           = "application/vnd.sun.xml.impress";
        $cfgContentTypes['pdf']           = "application/pdf";
        $cfgContentTypes['ps']            = "application/postscript";
        $cfgContentTypes['swf']           = "application/x-shockwave-flash";
        $cfgContentTypes['dvi']           = "application/x-dvi";
        $cfgContentTypes['csv']           = "text/csv";
        $cfgContentTypes['ics']           = "text/calendar";
        if (isset($cfgContentTypes[$type])) {
            $this->ctx->set('content-type', $cfgContentTypes[$type]);
        } else {
            $this->ctx->set('content-type', 'text/html');
        }
    }

    function sendContentType($type = '') {
        if (!$this->ctx->get('content-type-sent') && !headers_sent()) {
            $this->ctx->set('content-type-sent', true);
            if ($type) {
                header('Content-Type: '.$type);
            } else {
                header('Content-Type: '.$this->ctx->get('content-type'));
            }
        }
    }

    /**
     * Main method of the filter. Start processing
     *
     * @see phpinchains_Filter::doFilter()
     */
    function doFilter(& $env, & $chain) {
        $this->viewmapping->setEnv($env);
        $this->viewmapping->setMedium($env->getAttribute('__mediumParam__'));
        $this->setContentType($env->getAttribute('__mediumParam__'));
        $this->env = & $env;
        // Wrapp request
        $actionform = new phpinchains_ActionForm(array_merge($env->getParams(), $env->getFiles()));
        if ($parameters = $env->getAttribute('__parameterParam__')) {
            $parts = split('/', $parameters);
            while (count($parts)) {
                $name  = array_shift($parts);
                $value = urldecode(array_shift($parts));
                $actionform->set($name, $value);
            }
        }
        $this->dispatch($env->getAttribute('__actionParam__'), $actionform, true);

        // Output processing at the end of all nesting calls (dispatching completed)
        $this->processOutput($actionform);

        //TODO Make output processing working with nested apps: don't process, if app is nested => indicator to tell app that it is root app or not.

        // writing back cached basedir for nested apps
        if (isset ($this->basedirCache))
            $GLOBALS['MODEL2_BASEDIR'] = $this->basedirCache;
        $chain->doFilter($env);
    }

    function processOutput (& $actionform) {
        $jsout = & $this->ctx->getJSOutputWriter();
        $out   = & $this->ctx->getOutputWriter();
        $jscode = $jsout->getBuffer();
        $code = $out->getBuffer();
        if ($jscode) {
            $this->ctx->debug('forward: jscode > jsresponse '.$actionform->get('jsresponse'));
            if ($actionform->get('jsresponse') == 'true') {
                $code = str_replace("___jsheadercode___", $jscode, $code);
            }
            else {
                $str = "<script type=\"text/javascript\" language=\"Javascript\">\n<!--\n".$jscode."\n//-->\n</script>\n";
                if (preg_match("/<\/head/", $code)) {
                    $code = str_replace("</head", $str."</head", $code);
                }
                else {
                    $code = $str.$code;
                }
            }
        }
        else
            $code = str_replace("___jsheadercode___", '', $code);
        if ($this->printOutput) {
            print $code;
            $this->ctx->getOutputWriter()->clearBuffer();
        }
    }

    /**
     * Dispatch an action by its mapping
     *
     * This method is call by the action dispatcher, but it can be used to redirect
     * every action you want, except the ones, that are not mapped (the ones that are
     * mapped by a view mapping: they are not accessible from the outside)
     *
     * @param string                 $actionmapping  Mapping configured in action-conf.xml
     * @param phpinchains_ActionForm $actionform     Wrapps a simulated request.
     * @param bool                   $outsidecall    Set true at initial call of this method
     * @see ActionDispatcherFilter::callAction()
     */
    function dispatch($actionmapping, & $actionform, $outsidecall = false) {
        $this->ctx->debug('ActionDispatcher::dispatch action:'.$actionmapping);
        if (!empty($actionmapping)) {
            if (empty ($this->actiondict[$actionmapping])) {
            // automatic methodmapping without configuration of each method
                $matches = array();
                $byp_class = ''; // class name used with bypassMapping set 'true'
                if (preg_match('/(\w+)::(\w+)$/', $actionmapping, $matches)
                    && $matches[2][0] != '_'                                                    // private methods in php4 version protected by leading _
                    && $matches[1][0] != '_'                                                    // private classes protected by leading _ (only callAction possible than)
                    && ((!empty($this->actiondict[$matches[1]])                                 // action has to be mapped
                    && !empty($this->actiondict[$matches[1]]['file']['methodmapping'])
                    && $this->actiondict[$matches[1]]['file']['methodmapping'] == true) // \w+::\w+ pattern only allowed if methodmapping activated
                    || ($this->bypassMapping                                               // bypass-mapping configuration param is set 'true'
                    // some variables that are needed for location action an further on
                    && ($byp_class = strtoupper($matches[1][0]).substr($matches[1], 1).'Action')
                    && file_exists($this->basedir.'action/'.$byp_class.$this->fileending)
                    )
                    )
                    && ($matches[2] == 'execute'                                                // standard action method
                    || $matches[2] == 'index'                                                // standard action method
                    || !method_exists(new phpinchains_WebAction(), $matches[2])))           // do not allow defined methods of WebAction to be called
                {
                    if (empty($this->actiondict[$matches[1]])) {
                        $this->actiondict[$matches[1]] = array(
                            'actionmapping' => $matches[1],
                            'class' => $byp_class,
                            'file' => array (
                            'class' => $byp_class,
                            'methodmapping' => true,
                            'bypassViewMapping' => true
                            )
                        );
                    }
                    $cache = $this->viewmapping->getAction();
                    $this->viewmapping->setAction($matches[1]);
                    $this->actiondict[$matches[1]]['file']['method'] = $matches[2];
                    $this->doDispatch($this->actiondict[$matches[1]], $actionform, false, $outsidecall);
                    $this->viewmapping->setAction($cache);
                }
                else // Action not found in mapping
                {
                    $cache = $this->viewmapping->getAction();
                    $this->viewmapping->setAction($this->defaulterroraction['class']);
                    $this->env->setAttribute("errorLocation", "doDispatch");
                    $this->env->setAttribute("errorCause", "ActionNotMapped");
                    $this->env->setAttribute("errorSubject", $actionmapping);
                    $this->env->getAttribute("errorRequest", $actionform);
                    $this->doDispatch($this->defaulterroraction, $actionform, false, $outsidecall);
                    $this->viewmapping->setAction($cache);
                }
            }
            else {
                $cache = $this->viewmapping->getAction();
                $this->viewmapping->setAction($actionmapping);
                $this->doDispatch($this->actiondict[$actionmapping], $actionform, false, $outsidecall);
                $this->viewmapping->setAction($cache);
            }
        }
        else {
            $cache = $this->viewmapping->getAction();
            $this->viewmapping->setAction($this->defaultaction['class']);
            $this->doDispatch($this->defaultaction, $actionform, false, $outsidecall);
            $this->viewmapping->setAction($cache);
        }
    }

    /**
     * Dispatching helper method
     *
     * @param string                 $actionclass  Action class' name
     * @param phpinchains_ActionForm $actionform   Wrapps a simulated request (parameters).
     * @param bool                   $erroraction  Does this incarnation handle an error action?
     * @param bool                   $outsidecall  Set true at initial call of this method
     */
    function doDispatch($actionclass, & $actionform, $erroraction = false, $outsidecall = false) {
        $file = "";
        $method = "";
        if (is_array($actionclass['file'])) // class and method given
        {
            $file   = $actionclass['file']['class'];
            if (isset($actionclass['file']['method']))
                $method = $actionclass['file']['method'];
        }
        else {
            $file = $actionclass['file'];
            $method = 'execute';
        }
        $this->ctx->debug('ActionDispatcher::doDispatch action:'.$file.' method:'.$method);
        if (file_exists($this->basedir.'action/'.$file.$this->fileending)) {
            $this->invoke($file, $actionclass, $actionform, $method, $outsidecall);
        }
        elseif (!$erroraction) //this incarnation of doDispatch is not handling the error action (now created one wants)
        {
            if (defined("MODEL2_CREATE_CLASSES") && MODEL2_CREATE_CLASSES) {
                $this->generateStdAction($this->basedir.'action/'.$file.$this->fileending, $actionclass['class'], $method);
                $this->invoke($file, $actionclass, $actionform, $method);
            }
            else {
                $cache = $this->viewmapping->getAction();
                $this->viewmapping->setAction($this->defaulterroraction['class']);
                $this->env->setAttribute("errorLocation", "doDispatch");
                $this->env->setAttribute("errorCause", "ActionNotDefined");
                $this->env->setAttribute("errorSubject", $file);
                $this->env->setAttribute("errorRequest", $actionform);
                $this->doDispatch($this->defaulterroraction, $actionform, true);
                $this->viewmapping->setAction($cache);
            }
        }
        else {
            print "not even error default error action defined! wrong base dir? (".$this->basedir.'action/'.$file.$this->fileending.")";
        }
    }

    /**
     * Redirect to another action is this one is not responsible
     *
     * With the actionform a request can be simulated. Just set the needed parameters
     * in an actionform and pass it to the action.
     * The method name is optional. If not specified 'execute' will be invoked.
     *
     * @param string $actionclass Action class' name
     * @param ActionForm &$actionform The actionform which should be used.
     * @param string $method Name of the action method to call.
     * @see ActionDispatcherFilter::dispatch()
     */
    function callAction($actionclass, & $actionform, $method = '') {#
        $this->ctx->debug('ActionDispatcher::callAction action:'.$actionclass.'::'.$method);
        $action['file'] = $actionclass;
        $matches = array ();
        preg_match('/(\w+)$/', $actionclass, $matches);
        $action['class'] = $matches[1];
        if (file_exists($this->basedir.'action/'.$action['file'].$this->fileending)) {
            include_once ($this->basedir.'/action/'.$action['file'].$this->fileending);
            $actionobj = new $action['class']();
            $cache = $this->viewmapping->getAction();
            // if viewmapping with bypassViewMapping == true, than only the
            // class name is used as action mapping
            if ((isset($this->actiondict[$action['file']]['file']['bypassViewMapping'])
                && $this->actiondict[$action['file']]['file']['bypassViewMapping'])
                ||
                ($this->bypassMapping && empty($this->actiondict[$action['file']]))) {
                $this->viewmapping->setAction($action['file']);
            }
            else
                $this->viewmapping->setAction($action['file'].$method);
            $this->invoke($actionclass, $action, $actionform, $method);
            //$this->handleView($actionobj->executeAction($this, $this->env, $this->basedir, $this->viewmapping, $actionform, $method), $actionobj, $method, $actionform);
            $this->viewmapping->setAction($cache);
        } else {
            $this->ctx->debug('ActionDispatcher::callAction action:'.$actionclass.'::'.$method.' does not exist');
        }
    }

    /**
     * Do class loading and method invocation
     *
     * Common part of invocation from ActionDispatcher#dispatch and ActionDispatcher#callAction
     *
     * @param string                 $file         Filename (w/o extension) of the class
     * @param string                 $actionclass  Action class' name
     * @param phpinchains_ActionForm $actionform   The actionform which should be used.
     * @param string                 $method       Name of the action method to call.
     * @param bool                   $outsidecall  Set true at initial call of this method
     */
    function invoke ($file, $actionclass, & $actionform, $method, $outsidecall = false) {
        $this->ctx->debug('ActionDispatcher::invoke file:'.$file.' action:'.$actionclass['class'].'::'.$method);
        include_once ($this->basedir.'action/'.$file.$this->fileending);
        $actionobj = new $actionclass['class']();
        // first action in possible chain takes actionform with real params
        $params = $this->env->getParams();
        $params['dispatcherConfig'] = & $this->config;
        if (empty($method) || method_exists($actionobj, $method)) {
            $view = $actionobj->executeAction($this, $this->env, $this->basedir, $this->viewmapping, $actionform, $method, $outsidecall);
            $this->handleView($view, $actionobj, $method, $actionform);
        }
        else // action method missing
        {
        // create action method to class if MODEL2_CREATE_CLASSES and (view with same name does not exist or bypassMapping is not active)
            if (defined("MODEL2_CREATE_CLASSES") && MODEL2_CREATE_CLASSES
                && (!file_exists($this->basedir.'view/'.$this->viewmapping->getAction().'/'.$method.'.psp')
                || !isset($this->actiondict[$this->viewmapping->getAction()]['file']['bypassViewMapping'])
                || !$this->actiondict[$this->viewmapping->getAction()]['file']['bypassViewMapping']))
            // add action method to class only if view does not exists or bypassing is deactivated
            {
                $this->addActionMethod ($this->basedir.'action/'.$file.$this->fileending, $actionclass['class'], $method);
                // evil trick
                eval("class Evil".$actionclass['class']." extends phpinchains_WebAction { function ".$method.' (& $env, & $viewmapping, & $actionform) {} }');
                $actionclass['class'] = 'Evil'.$actionclass['class'];
                $this->invoke($file, $actionclass, $actionform, $method);
            }
            else {
                if (file_exists($this->basedir.'view/'.$this->viewmapping->getAction().'/'.$method.'.psp')
                    && isset($this->actiondict[$this->viewmapping->getAction()]['file']['bypassViewMapping'])
                    && $this->actiondict[$this->viewmapping->getAction()]['file']['bypassViewMapping']
                ) {
                    $this->forward($this->viewmapping->getAction().'/'.$method.'.psp', $actionform);
                }
                else {
                    $cache = $this->viewmapping->getAction();
                    $this->viewmapping->setAction($this->defaulterroraction['class']);
                    $this->env->setAttribute("errorLocation", "doDispatch");
                    $this->env->setAttribute("errorCause", "ActionMethodNotDefined");
                    $this->env->setAttribute("errorSubject", $file.'::'.$method);
                    $this->env->getAttribute("errorRequest", $actionform);
                    $this->doDispatch($this->defaulterroraction, $actionform, true);
                    $this->viewmapping->setAction($cache);
                }
            }
        }
    }

    /**
     * Handles the view forwarding.
     *
     * If view does not end with .php or .psp than view is looked up in viewmapping.
     * If WebAction#forward was called than no further view handling is necessary.
     * If WebAction#forward was not called, no view is provided or found an error
     * occurs unless filterParam no-view-error was set true.<br/>
     * <br/>
     * If a modified request should be provided with an actionform from within the
     * action's action method, the variant with calling forward has to be used.<br/>
     * <br/>
     * With bypassViewMapping equal 'true' the mapping name or the actionname
     * is put at the beginning of the view path. If one wants to forward to an other
     * view than those in this folder, the return value of the action has to start
     * with a slash.<br/>
     * e.g.<br/>
     * Action name: TestAction, mapping: test<br/>
     * return 'test'; => view/test/test.psp is forwarded to<br/>
     * return 'sub/test'; => view/test/sub/test.psp is forwarded to<br/>
     * return '/other/ping'; => view/other/ping.psp is forwarded to<br/>
     *
     * @param string $view Name of file or mapping or empty
     * @param WebAction $actionobj Action object was executed
     * @param string $method Name of the method was called
     */
    function handleView($view, & $actionobj, $method, & $actionform) {
        if (!$actionobj->useReturnValue())
            return;
        $this->ctx->debug('ActionDispatcher::handleView view:'.$view.' action: '.get_class($actionobj).'::'.$method);
        $bypass = ((isset($this->actiondict[$this->viewmapping->getAction()]) && is_bool($this->actiondict[$this->viewmapping->getAction()]['file']['bypassViewMapping'])
            && $this->actiondict[$this->viewmapping->getAction()]['file']['bypassViewMapping'] == true)
            || (empty($this->actiondict[$this->viewmapping->getAction()])
            && $this->bypassMapping
            && !in_array($this->viewmapping->getAction(), array('DefaultAction', 'DefaultErrorAction'))
            )
        );
        $viewC = $view;
        if ($this->viewByMethod && empty($view)) {
            $view = $method?$method:'index';
        }
        $extension = substr($view, -4, 4);
        // should view mapping be bypassed
        if ($bypass) {
            if ($view[0] == '/')
                $view = substr($view, 1).'.psp';
            else {
                $actionname = $this->viewmapping->getAction();
                // Make views unsed in a view forwarded from callAction called
                // action use the same folder dispatch does (with methodmapping="true")
                if (substr($actionname, -6, 6) == 'Action'
                    && !$this->compatibilityMode) {
                    $actionname = substr($actionname, 0, -6);
                    $actionname[0] = strtolower($actionname[0]);
                }
                $view = $actionname.'/'.$view.'.psp';
            }
        }
        elseif (!empty($view) && $extension != '.php' && $extension != '.psp')
            $view = $this->viewmapping->findForward($view);

        if (empty($view) && !$this->noViewError) // no view found and error turned on
        {
            $cache = $this->viewmapping->getAction();
            $this->viewmapping->setAction($this->defaulterroraction['class']);
            $this->env->setAttribute("errorLocation", "handleView");
            $this->env->setAttribute("errorCause", "NoViewFound");
            $this->env->setAttribute("errorSubject", ($method?$method:'execute').'=>'.$viewC);
            $this->env->getAttribute("errorRequest", $actionform);
            $this->doDispatch($this->defaulterroraction, $actionform, true);
            $this->viewmapping->setAction($cache);
        }
        elseif (!empty($view)) // view found
        {
            $this->forward($view, $actionform);
        }
    }

    /**
     * Forwarding to a view. A view is either a flat php file that has access to the
     * variables $env with the environment wrapper and $actionform, somthing like a bean
     * in Java.
     *
     * @param string $view Name of the view php file (without file extension; the file
     *              must have extension .inc.php) or the view PSP file (with extension
     *              .psp; file has to have the same name)
     * @param ActionForm $actionform Actionform object to transport data to the view
     * @param boolean $validatorCall Has validator called this method?
     */
    function forward($view, & $actionform, $validatorCall = false) {
        $this->ctx->debug('ActionDispatcher::forward view:'.$view.' validator call:'.$validatorCall);
        if (preg_match('/^[\w\/\.]+$/', $view)) // valid viewname?
        {
            $env = & $this->env;
            $extension = substr($view, -4, 4);
            $source = $this->basedir.'view/'.$view;
            if ($extension == '.psp') // is psp? (php views are possible too)
            {
                if ($this->compileDir == null)
                    $dest   = $this->basedir.'view/'.$view.'.php';
                else
                    $dest   = $this->compileDir.str_replace('/', '__',$view).'.php';
                $viewname = substr($view, 0, -4);
                // if psp does not exists, create it (each view which is configured shoud be accessible)
                if (!file_exists($source) && MODEL2_CREATE_PSP) {
                    $this->generateStdPSP($source);
                }
                // if compiled php file exists, check the modification time: psp could have changed
                if (file_exists($dest) && !MODEL2_FORCE_COMPILE) {
                    $cfile = stat($dest);
                    $sfile = stat($source);
                    // mtime comparison
                    if ($cfile[9] < $sfile[9])
                        $this->compilePSP($source, $dest, $viewname);
                }
                else // if compiled php file does not exists, create it anyway
                {
                    $this->compilePSP($source, $dest, $viewname);
                }
                include_once ($dest);
                // Validator can call this method, just to be sure the compiled file is up-to-date
                if (!$validatorCall) {
                    $this->sendContentType();
                    $classname = str_replace('.', '_dot_',str_replace('/', '__',$viewname)).'PSPPage';
                    $obj = new $classname();
                    $actionform->set('basedir', $this->basedir);
                    $actionform->set('compiledir', $this->getCompileDir());
                    // TODO find out what this line is good for or delete it
                    //preg_replace('#</head#i', '"; print \'<script type="text/javascript" language="Javascript">\'."\n";'."\n".$parser->getJSCode()."\n".'print "</script>\n</head', $parser->getCode())
                    $out = $obj->render($this, $this->env, $actionform, $this->basedir);
                    $jsout = & $this->ctx->getJSOutputWriter();
                    $jsout->write($obj->getJSCode($this, $this->env, $actionform));
                }
            }
            else
                include ($source);
        }
        else
            print ("<strong>View error!</strong>");
    }

    /**
     * Shall output be printed after execution of an action?
     *
     * Used by execute tag to disable printout of a nested action.
     *
     * @param boolean $do Print or not.
     */
    function setPrintOutput ($do) {
        $this->printOutput = $do;
    }

    /**
     * Shall output be printed after execution of an action?
     *
     * Used by execute tag to disable printout of a nested action.
     *
     * @param boolean $do Print or not.
     */
    function getPrintOutput () {
        return $this->printOutput;
    }

    /**
     * Validate specified form of the given psp page (without extension)
     *
     * @param ActionForm $actionform Actionform used in calling action
     * @param string $psp Path to psp relative to view directory (w/o extension)
     * @param string $form Name of the form to validate
     * @return boolean Validation successful?
     */
    function validate (& $actionform, $psp, $form, $field) {
        $obj = $this->getPSPObj($psp);
        return $obj->validate($this, $this->env, $actionform, $form, $this->basedir, $field);
    }

    /**
     * Get an object from a psp
     *
     * Used by phpinchains_WebAction#wizzard and
     * phpinchains_filter_ActionDispatcherFilter#validate
     *
     * @param string $psp Path to psp relative to view directory (w/o extension)
     * @return phpinchains_psp_PSPPage Object of given psp
     */
    function getPSPObj ($psp) {
        $this->forward($psp.'.psp', new phpinchains_ActionForm(), true);
        $classname = str_replace('.', '_dot_',str_replace('/', '__',$psp)).'PSPPage';
        return new $classname();
    }

    /**
     * Get class name for psp class derived from absolute filename
     *
     * @param string $filename Absolute file name of psp file
     */
    function getPSPClassName($filename)
    {
    	if (!preg_match('#/^'.$this->basedir.'/#', $filename)) {
    		$start = 0;
    		while (!empty($this->basedir[$start]) && $filename[$start] == $this->basedir[$start]) {
    			$start++;
    		}
	        return str_replace('.', '_dot_',str_replace('/', '__', substr($filename, $start, -4)));
    	} else {
	        return str_replace('.', '_dot_',str_replace('/', '__', substr($filename, strlen($this->basedir)+5, -4)));
    	}
    }

    /**
     * Compiles the PSP and saves the result for further execution.
     * @param string $sfile Source file name
     * @param string $cfile Compiled destination file name
     * @param string $viewname Name of the view
     */
    function compilePSP($sfile, $cfile, $viewname) {
        $ctx = & phpinchains_ApplicationContext::getInstance();
        $env = & $ctx->getEnv();
        $env->setAttribute('__lastView', $viewname);
        if (!file_exists($sfile)) {
            die("<strong>FATAL ERROR:</strong> View $viewname.psp not found!\n");
        }
        $lex = new phpinchains_parser_psp_PSPLexer($GLOBALS['MODEL2_NS'], join("", file($sfile)), $sfile);
        $lex->lex();
        $parser = new phpinchains_parser_psp_PSPParser($lex, $this->basedir);
        $parser->parse();
        if (file_exists($cfile) && !is_writable($cfile)) {
            print "<strong>FATAL ERROR:</strong> File '$cfile' is not writeable!<br/>";
            return;
        }
        $fp = fopen($cfile, 'w');
        fputs($fp, '<'."?php\n" .
            "m2import('phpinchains/util/BundleTool.php');\n" .
            "m2import('phpinchains/psp/PSPPage.php');\n" .
            "m2import('phpinchains/psp/OutputWriter.php');\n" .
            "m2import('phpinchains/tagsupport/HtmlInput.php');\n" .
            "m2import('phpinchains/tagsupport/URL.php');\nm2import('phpinchains/tagsupport/functions.inc.php');\n" .
            "class ".str_replace('.', '_dot_',str_replace('/', '__',$viewname))."PSPPage extends phpinchains_psp_PSPPage \n" .
            "{\n" .
            "   var \$validatormap;\n" .
            "   var \$objcache;\n" .
            "   var \$relativeFilePath = '".substr($sfile, strlen($this->initbasedir))."';\n" .
            "   var \$fieldMap = ".var_export($parser->getFieldMap(), true).";\n\n".
            "   var \$fileFieldMap = ".var_export($parser->getFileFieldMap(), true).";\n\n".
            "   var \$requestableDataCode = array(".$parser->getRequestableDataCode().");\n\n".
            // function validate
            '   function validate (& $dispatcher, & $env, & $actionform, $formname, $basedir, $fieldname = "")'."\n" .
            "   {\n" .
            "       \$this->basedir = \$basedir;\n".
            "       \$this->dispatcher = & \$dispatcher;\n".
            "       include(\$basedir.'action-config.xml.php');\n".
            "       \$this->validatormap = \$validators;\n".
            "       \$validationErrors = & \$actionform->getRef('validationErrors');\n".
            "       \$validationErrors->setForm(\$formname);\n" .
            "       ".$parser->getValidationCode()."\n" .
            "       return !\$validationErrors->hasErrors();\n" .
            "   }\n".
            '   function render (& $dispatcher, & $env, & $actionform, $basedir)'."\n" .
            "   {\n" .
            "       \$this->basedir = \$basedir;\n".
            "       \$this->dispatcher = & \$dispatcher;\n".
            "       \$ctx = & phpinchains_ApplicationContext::getInstance();\n".
            "       \$out = & \$ctx->out;\n".
            $parser->getFormSelectionCode().
            "       ".$parser->getCode()."\n" .
            "       return \$out;\n" .
            "   }\n" .
            '   function getJSCode (& $dispatcher, & $env, & $actionform)'."\n" .
            "   {\n" .
            "       \$this->dispatcher = & \$dispatcher;\n".
            "       \$jscode = \"".$parser->getJSCode()."\";\n" .
            "       return \$jscode;\n" .
            "   }\n" .
            $parser->getFormsCode().
            "}\n?".">");
        fclose($fp);
    }

    /**
     * Generates a standard hello world psp if a called psp does not exist
     *
     * if template.psp exists in basedir this file is used, else
     *
     * @param string filename Filename of the psp to generate
     */
    function generateStdPSP ($filename) {
        phpinchains_util_DirTool::createDirToFile($filename, '', (defined('MODEL2_CHMOD')?MODEL2_CHMOD:0755));
        if (file_exists($this->basedir."template.psp")) {
            copy($this->basedir."template.psp", $filename);
        }
        else {
            $fp = fopen($filename, 'w');
            fputs($fp, '
<html>
    <head>
        <title>Your Title: '.$filename.'</title>
    </head>                  
    <body>
        <h1>Generated PSP: '.$filename.'</h1>
    </body>
</html>                            
                    ');
            fclose($fp);
        }
        if (defined("MODEL2_CHMOD"))
            chmod($filename, MODEL2_CHMOD);
    }

    /**
     * Generates an action class
     *
     * if template.php is found in basedir, it is used as template
     * TODO ban to delegated class
     */
    function generateStdAction ($filename, $classname, $methodname) {
        if (empty($methodname))
            $methodname = 'execute';
        if (file_exists($this->basedir."template.php")) // load and use the template
        {
            $fp = fopen($filename, 'w');
            fputs($fp, '<'."?php\n".sprintf(
                file_get_contents($this->basedir."template.php"),
                $classname, $methodname, $classname, $methodname, $methodname).
                "\n?".'>');
            fclose($fp);
        }
        else // use std code template
        {
            $fp = fopen($filename, 'w');
            fputs($fp, $this->getCode($classname, $methodname, $classname, $methodname, $methodname, $methodname));
            fclose($fp);
        }
        if (defined("MODEL2_CHMOD"))
            chmod($filename, MODEL2_CHMOD);
    }

    /**
     * Adds an action method to a given class
     *
     * if template.php is found in basedir, it is used as template
     * TODO ban to delegated class
     */
    function addActionMethod ($filename, $classname, $methodname) {
        if (empty($methodname))
            return false;
        $content = file_get_contents($filename);
        if (file_exists($this->basedir."method_template.php")) {
            $fp = fopen($filename, 'w');
            fputs($fp,
                preg_replace('/\s*}\s*\?>\s*$/m',
                sprintf(
                file_get_contents($this->basedir."method_template.php"),
                $methodname, $classname, $methodname, $methodname
                )."\n}\n?".">",
                $content
                )
            );
            fclose($fp);
        }
        else {
            $funccode = '

    /**
     * generated action method
     */
    function '.$methodname.' (& $env, & $viewmapping, & $actionform)
    {
        $af = new phpinchains_ActionForm();
        print "<strong>Change code of generated action method '.$classname.'::'.$methodname.'!</strong>";
        // your code ...
    }

}
?'.'>';
            $fp = fopen($filename, 'w');
            fputs($fp, preg_replace('/\s*}\s*\?>\s*$/m', $funccode, $content));
            fclose($fp);
        }
    } // addActionMethod

    // TODO ban to delegated class
    function getCode ($repl1, $repl2, $repl3, $repl4, $repl5, $repl6, $isfile = false) {
        $code = '
/**
 * Generated Action Class
 *
 */
class %s extends phpinchains_WebAction
{

    /**
     * generated action method
     */
    function %s (& $env, & $viewmapping, & $actionform)
    {
        $af = new phpinchains_ActionForm();
        print "<strong>Change code of generated action method %s::%s!</strong>";
        // your code ...
    }
    
}
';
        if ($isfile)
            return sprintf("<"."?php\n".$code.'?'.">", $repl1, $repl2, $repl3, $repl4, $repl5, $repl6);
        else
            return sprintf($code, $repl1, $repl2, $repl3, $repl4, $repl5, $repl6);
    } // getCode

    function isPSPClassCompiler () {
        return true;
    }

}
?>